var defaultValue = function(x = 0,  y = 1){
  //参数变量是默认声明的，所以不能用let或const再次声明。
  //let x; //Duplicate declaration "x"
}
//通常情况下，定义了默认值的参数，应该是函数的尾参数。因为这样比较容易看出来，到底省略了哪些参数。如果非尾部的参数设置默认值，实际上这个参数是没法省略的
// 例二
function funcLen(x, y = 5, z) {//函数length = 1; 
  return [x, y, z];
}
//funcLen(1, ,2) // 报错
funcLen(1, null ,2)//null 原样输出
funcLen(1, undefined, 2) // [1, 5, 2] 
//观察编译后的代码可知 只有传入undefined，才触参数等于默认值，null是没有这个效果。

//函数的length属性,指的是该函数预期传入的参数个数.
//指定了默认值后，length属性将失真。变成了有默认值的参数 以前有几个参数
//console.log(f.length)


function defaultParam(y = x) {//函数调用时，y的默认值变量x尚未在函数内部生成，所以x指向全局变量
  let x = 2;
  console.log(y);
}
//defaultParam(); //运行报错 x is not defined

var foo = 'outer'; //在最上面,正常运行
function defaultFunc1(func1 = () => foo) { //指向outer
  let foo = 'inner1';
  console.log(func1());
}

defaultFunc1();
//var foo = 'outer'; //var 存在变量提升, 输出undefined
//let foo = 'outer';//报错

function defaultFunc2(func2 = (function() {return foo})) {
  let foo = 'inner2';
  console.log(func2());
}
defaultFunc2(); //inner2 
//实际上这两个函数应该会报错的. 如果是ES6的话, 但编译后, 就不是了

//利用参数默认值，可以指定某一个参数不得省略，如果省略就抛出一个错误。
function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

//foo();

//ES6引入rest参数（形式为“...变量名”），用于获取函数的多余参数，这样就不需要使用arguments对象了
//rest参数搭配的变量是一个数组，该变量将多余的参数放入数组中。

// 报错 rest参数必须是最后一个参数
// function f(a, ...b, c) {
// }

//函数的length属性，不包括rest参数。
function restLen(a, ...rest) {}
console.log(restLen.length)  // 1


//扩展运算符（spread）是三个点（...）。它好比rest参数的逆运算，将一个数组转为用逗号分隔的参数序列。
// ES6的写法
Math.max(...[14, 3, 77]) //编译后Math.max.apply(Math, [14, 3, 77]);

// 等同于
Math.max(14, 3, 77);

//应用
// ES5的合并数组
let arr1 = [], arr2 = [], arr3 = [];
[].concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(rest); // [2, 3, 4, 5]

console.log([...'hello']) //可以正确返回字符串长度,凡是涉及到操作32位Unicode字符的最好都使用...处理下


//函数的名字 .name
//const log = (msg) => console.log(msg); //注意编译后是有名字的函数
const log = function(msg) {console.log(msg)}; //编译后还是有名字的函数
//所以log.name = 'log'
log(log.name);
//在有es5中,如在浏览器中, 上面的结果为""

log((new Function).name)//anonymous
log((function(){}).bind({}).name); //bound


//箭头函数, 简化回调
// 正常函数写法
[1,2,3].map(function (x) {
  return x * x;
});

// // 箭头函数写法
[1,2,3].map(x => x * x);

//重要一点:函数体内的this对象，就是定义时所在的对象，而不是使用时所在的对象.在箭头函数中,this是固定不变的
window = global;
window.s2 = 2;
function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  let arrowFn = () => {
    this.s1++;
  };
  
  // 普通函数
  let normalFn = function(){
    this.s2 ++;
  }.bind(this);
  
  arrowFn();
  normalFn();
  
  setTimeout(function(){
    //window.s2 = 0; //假如没有这句话,window.s2的值是不不确定的
   // console.log('in setTimeout ' + ++this.s2);
    //console.log('in setTimeout ' + window.s2);
  }, 500);
};    
 
var timer = new Timer();

//为什么值会不断变化?         
// setInterval(() => {
//   console.log('s1: ', timer.s1); 
//   console.log('s2: ', timer.s2, ',window.s2: ', window.s2);
// }, 100);
  console.log('s1: ', timer.s1); 
  console.log('s2: ', timer.s2, ',window.s2: ', window.s2);
  


//important: tail call 尾调用:指某个函数的最后一步是调用另一个函数。
function f(x){
  return g(x);
}
//尾调用不一定出现在函数尾部，只要是最后一步操作即可。
function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

//但以下三种情况不是:
// 情况一
function f(x){
  let y = g(x);
  return y;
}

// 情况二
function f(x){
  return g(x) + 1;
}

// 情况三
function f(x){
  g(x);
}

//尾调用优化:不再用到外层函数的内部变量，内层函数的调用帧就会取代外层函数的调用帧
//如果所有函数都是尾调用，那么完全可以做到每次执行时，调用帧只有一项
function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}

// 等同于
function f() {//尾调用优化成功
  return g(3);
}

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a); //无法尾调用优化,因为内层函数使用了外层函数的变量
}

//尾递归
function Fibonacci (n) {
  if ( n <= 1 ) {return 1};

  return Fibonacci(n - 1) + Fibonacci(n - 2); //调用未优化
}

//log('Fibonacci: ' + Fibonacci(48));//浏览器很慢了
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac1};

  return Fibonacci2 (n-1 , ac2 , ac1 + ac2);
}
log('尾递归优化后的Fibonacci(100) : ' + Fibonacci2(100)); //1s内出结果

//尾递归的实现，往往需要改写递归函数，确保最后一步只调用自身。做到这一点的方法，就是把所有用到的内部变量改写成函数的参数。
//比如上面的例子，阶乘函数 factorial 需要用到一个中间变量 total ，那就把这个中间变量改写成函数的参数。
//方法一:
function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

function factorial(n) {//内部用另一个函数传入中间变量total
  return tailFactorial(n, 1);
}
//方法二:
//函数式编程有一个概念，叫做柯里化（currying），意思是将多参数的函数转换成单参数的形式。
function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);
  };
}

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}
factorial = currying(tailFactorial, 1);

//方法三: es6函数参数默认值. 非常简单
function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

//总结一下，递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令，所有的循环都用递归实现，这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言（比如Lua，ES6），只需要知道循环可以用递归代替，而一旦使用递归，就最好使用尾递归。

//ES6的尾调用优化只在严格模式下开启，正常模式是无效的。
//这是因为在正常模式下，函数内部有两个变量，可以跟踪函数的调用栈。

//func.arguments：返回调用时函数的参数。
//func.caller：返回调用当前函数的那个函数。
//尾调用优化发生时，函数的调用栈会改写，因此上面两个变量就会失真。严格模式禁用这两个变量，所以尾调用模式仅在严格模式下生效。
